using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO.Packaging;
using System.IO;
using System.Xml;
using System.Collections;

namespace Extractor
{
    /// <summary>
    /// Main class for the extractor form dialog
    /// </summary>
    public partial class Extractor : Form
    {
        /// <summary>
        /// keeps track of the loaded file name
        /// </summary>
        private string loadFileName;

        /// <summary>
        /// keeps track of all the URIs for the embedded objects
        /// </summary>
        private ArrayList embeddingURIs;

        /// <summary>
        /// Constructor for the extractor standalone tool
        /// </summary>
        public Extractor()
        {
            InitializeComponent();
        }

        /// <summary>
        /// show user a File Open dialog with the specified OOXML files set as the filter
        /// </summary>
        /// <returns></returns>
        private string GetOpenPath()
        {
            OpenFileDialog sfd = new OpenFileDialog();
            sfd.AddExtension = true;
            //Get only OOXML files
            sfd.Filter = "All Files (*.docx, *.docm, *.xlsx, *.xlsm, *.pptx, *.pptm)|*.docx;*.docm;*.xlsx;*.xlsm;*.pptx;*.pptm";
            sfd.CheckPathExists = true;

            sfd.ShowDialog();

            return sfd.FileName;
            // return the filename and the path in which the user wants to create the file
        }

        /// <summary>
        /// Open an OOXML file to see if there are any OOXML embedded objects
        /// </summary>
        /// <param name="sender">The object which raised the event</param>
        /// <param name="e">The arguments for the object which raised the event</param>
        private void load_Click(object sender, EventArgs e)
        {
            try
            {
                //Let user select the file he needs to extract embedded OOXML files from
                loadFileName = GetOpenPath();

                //make sure a file name is specified 
                if (loadFileName != "")
                {
                    embeddingURIs = new ArrayList();

                    extract.Enabled = false;
                    //clear contents of the display list box 
                    displayListBox.Items.Clear();

                    string partURI = "";
                    
                    Package toExtract = Package.Open(loadFileName);

                    //grab the parts within the OOXML package and check if there are any embedded OOXML objects
                    foreach (PackagePart pPart in toExtract.GetParts())
                    {
                        partURI = pPart.Uri.OriginalString;

                        //check to see if part is an embedded object
                        if (isEmbeddingOOXML(partURI))
                        {
                            //if so display embedded object in display list box
                            displayListBox.Items.Add(partURI);
                            embeddingURIs.Add(pPart.Uri);
                            //enable extract command to allow users to extract embedded object(s)
                            extract.Enabled = true;
                        }
                    }
                    toExtract.Close();

                    //tell user no embdedded objects were found
                    if (displayListBox.Items.Count == 0)
                        displayListBox.Items.Add("There are no embedded Office Open XML objects in the file");
                }
                //empty file
                else
                {
                    displayListBox.Items.Clear();
                    extract.Enabled = false;
                }
            }
            catch (System.IO.FileNotFoundException exc)
            {
                MessageBox.Show(exc.FileName);
            }
            catch (System.IO.IOException ex)
            {
                MessageBox.Show(ex.Message);
            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// check to see if OOXML part is an embdedded OOXML object
        /// </summary>
        /// <param name="embeddedName">The part name path, which can be an embedded object path</param>
        /// <returns>True if the specified part is an OOXML embedded object</returns>
        private bool isEmbeddingOOXML(String embeddedName)
        {
            bool isOOXML = false;
            string lastCharacters = "";

            lastCharacters = embeddedName.Substring(embeddedName.Length - 4, 4);

            //check to see if extension of embedded object is OOXML
            switch (lastCharacters)
            {

                case "docx":
                    isOOXML = true;
                    break;
                case "docm":
                    isOOXML = true;
                    break;
                case "xlsx":
                    isOOXML = true;
                    break;
                case "xlsm":
                    isOOXML = true;
                    break;
                case "xlsb":
                    isOOXML = true;
                    break;
                case "pptx":
                    isOOXML = true;
                    break;
                case "pptm":
                    isOOXML = true;
                    break;
                default:
                    isOOXML = false;
                    break;

            }

            return isOOXML;

        }

        /// <summary>
        /// Stream is a readStream and write out a writeStream
        /// </summary>
        /// <param name="readStream">A stream representing the embedded OOXML object</param>
        /// <param name="writeStream">A write stream representing a file for the embedded object</param>
        private void ReadWriteStream(Stream readStream, Stream writeStream)
        {
            int Length = 256;
            Byte[] buffer = new Byte[Length];
            int bytesRead = readStream.Read(buffer, 0, Length);
            // write the required bytes
            while (bytesRead > 0)
            {
                writeStream.Write(buffer, 0, bytesRead);
                bytesRead = readStream.Read(buffer, 0, Length);
            }
            readStream.Close();
            writeStream.Close();
        }

        /// <summary>
        /// Extract all of the embedded OOXML objects found in the loaded file
        /// </summary>
        /// <param name="sender">The object which raised the event</param>
        /// <param name="e">The arguments for the object which raised the event</param>
        private void extract_Click(object sender, EventArgs e)
        {

            //open the OOXML package
            System.IO.Packaging.Package package = System.IO.Packaging.Package.Open(loadFileName, System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite);

            //grab the path of the loaded file name
            string pathPart = loadFileName.Substring(0, loadFileName.LastIndexOf('\\') + 1);

            //grab the original file name
            string originalFileName = loadFileName.Substring(loadFileName.LastIndexOf('\\') + 1, loadFileName.LastIndexOf('.') - loadFileName.LastIndexOf('\\') - 1);

            //check each part uri in the package
            foreach (Uri partURI in embeddingURIs)
            {
                string[] parts = partURI.ToString().Split('/');
                string fileNameMinusExtension = parts[parts.Length - 1].Substring(0, parts[parts.Length - 1].LastIndexOf('.'));
                string extension = parts[parts.Length - 1].Substring(parts[parts.Length - 1].LastIndexOf('.'));
                string extractFileName = pathPart + fileNameMinusExtension + " in " + originalFileName + extension;

                //check to see if the extracted file name would exist and if so generate a unique file name
                extractFileName = checkFileName(pathPart, fileNameMinusExtension, extension, originalFileName, extractFileName);

                //grab the necessary part
                PackagePart embeddingPart = package.GetPart(partURI);
                System.IO.Stream partStream = embeddingPart.GetStream();


                // create a write stream
                FileStream writeStream = new FileStream(extractFileName, FileMode.Create, FileAccess.Write);


                // write to the stream
                ReadWriteStream(partStream, writeStream);

                //resolve the bug in embedded Excel objects
                if (extractFileName.EndsWith("xlsx") || extractFileName.EndsWith("xlsm"))
                {
                    //to make the workbook visible
                    updateExcel(extractFileName);
                }

            }
            package.Close();
            MessageBox.Show("Embedded OOXML objects are extracted to - " + pathPart);

        }

        /// <summary>
        /// Find a unique file name for the specified path and file
        /// </summary>
        /// <param name="pathPart">The path to which the file will be saved to</param>
        /// <param name="fileNameMinusExtension">The file name minus the extension</param>
        /// <param name="extension">the extension of the file</param>
        /// <param name="originalFileName">The original file name, which may already exist in the path</param>
        /// <param name="extractFileName">The proposed extract file name</param>
        /// <returns></returns>
        private string checkFileName(string pathPart, string fileNameMinusExtension, string extension, string originalFileName, string extractFileName)
        {
            if (!File.Exists(extractFileName))
                return extractFileName;
            else
            {
                for (int i = 1; ; i++)
                {
                    string tempFileName = pathPart + fileNameMinusExtension + " (" + i + ") " + " in " + originalFileName + extension;
                    if (!File.Exists(tempFileName))
                        return tempFileName;
                }
            }

        }

        /// <summary>
        /// Fixes the Excel bug that opens extracted embedded objects as hidden workbooks
        /// </summary>
        /// <param name="docFileName">The file name to which is an extracted Excel object</param>
        private void updateExcel(String docFileName)
        {
            try
            {
                const string documentRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";

                //open package
                Package package = Package.Open(docFileName, FileMode.Open, FileAccess.ReadWrite);
                PackagePart documentPart = null;

                // Get the document part
                foreach (System.IO.Packaging.PackageRelationship relationship in package.GetRelationshipsByType(documentRelationshipType))
                {
                    Uri documentUri = PackUriHelper.ResolvePartUri(new Uri("/", UriKind.Relative), relationship.TargetUri);
                    documentPart = package.GetPart(documentUri);

                    // There is only one document.
                    break;
                }

                //  Manage namespaces to perform Xml XPath queries.
                NameTable nt = new NameTable();
                XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(nt);
                xmlNamespaceManager.AddNamespace("workbook", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");

                XmlDocument xmlDocument = new XmlDocument();
                xmlDocument.Load(documentPart.GetStream());

                //change workbook visibility to make sure file opens fine in Excel
                XmlNode workbookViewNode = xmlDocument.SelectSingleNode("//workbook:workbookView", xmlNamespaceManager);
                workbookViewNode.Attributes["visibility"].Value = "visible";
                xmlDocument.Save(documentPart.GetStream());

                package.Close();
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }
}